﻿/*************************************************************************
 * Created by OhZee Creative, LLC
 * 2013
 * This code is licensed under Microsoft Public License (Ms-PL)
 * http://opensource.org/licenses/ms-pl
 * http://ozcontrols.codeplex.com/
 *************************************************************************/

namespace OhZeeCreative.UI.Controls
{
    using System;
    using System.ComponentModel;
    using OhZeeCreative.Helpers;

#if SILVERLIGHT
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media.Animation;    
#endif
#if NETFX_CORE
    using Windows.Foundation;
    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Media.Animation;
#endif

    /// <summary>
    /// RadialLayoutPanel is a panel that lays out its items in a radial manner whose start and end angles and radii can be specified.
    /// </summary>
    [EditorBrowsableAttribute(EditorBrowsableState.Always)]
	public sealed class RadialLayoutPanel : Panel
	{        
		public static readonly DependencyProperty RadiusBeginProperty = DependencyProperty.Register("RadiusBegin", typeof(double), typeof(RadialLayoutPanel), new PropertyMetadata(200.0, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty RadiusEndProperty = DependencyProperty.Register("RadiusEnd", typeof(double), typeof(RadialLayoutPanel), new PropertyMetadata(200.0, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty AngleBeginProperty = DependencyProperty.Register("AngleBegin", typeof(double), typeof(RadialLayoutPanel), new PropertyMetadata(-180.0, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty AngleEndProperty = DependencyProperty.Register("AngleEnd", typeof(double), typeof(RadialLayoutPanel), new PropertyMetadata(180.0, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty ChildAngleBeginProperty = DependencyProperty.Register("ChildAngleBegin", typeof(double), typeof(RadialLayoutPanel), new PropertyMetadata(0.0, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty ChildAngleDeltaProperty = DependencyProperty.Register("ChildAngleDelta", typeof(double), typeof(RadialLayoutPanel), new PropertyMetadata(0.0, GenericRenderInvalidatingPropertyChanged));

		public static readonly DependencyProperty EasingProperty = DependencyProperty.Register("Easing", typeof(EasingFunctionBase), typeof(RadialLayoutPanel), new PropertyMetadata(null, GenericRenderInvalidatingPropertyChanged));
        public static readonly DependencyProperty ChildAngleEasingProperty = DependencyProperty.Register("ChildAngleEasing", typeof(EasingFunctionBase), typeof(RadialLayoutPanel), new PropertyMetadata(null, GenericRenderInvalidatingPropertyChanged));
        public static readonly DependencyProperty NormalizedAngleProperty = DependencyProperty.RegisterAttached("NormalizedAngle", typeof(double), typeof(RadialLayoutPanel), new PropertyMetadata(double.NaN, GenericRenderInvalidatingPropertyChanged));

		public static readonly DependencyProperty ItemsSkippedProperty = DependencyProperty.Register("ItemsSkipped", typeof(int), typeof(RadialLayoutPanel), new PropertyMetadata(0, ItemsSkippedPropertyChanged));
		public static readonly DependencyProperty ItemsMaximumProperty = DependencyProperty.Register("ItemsMaximum", typeof(int), typeof(RadialLayoutPanel), new PropertyMetadata(int.MaxValue, ItemsMaximumPropertyChanged));

        public static readonly DependencyProperty OriginXProperty = DependencyProperty.Register("OriginX", typeof(double), typeof(RadialLayoutPanel), new PropertyMetadata(0.0));
        public static readonly DependencyProperty OriginYProperty = DependencyProperty.Register("OriginY", typeof(double), typeof(RadialLayoutPanel), new PropertyMetadata(0.0));
        public static readonly DependencyProperty CenterOriginProperty = DependencyProperty.Register("CenterOrigin", typeof(bool), typeof(RadialLayoutPanel), new PropertyMetadata(true, GenericRenderInvalidatingPropertyChanged));

        private static readonly Rect EmptyRect = new Rect();

		private double computedWidth;
		private double computedHeight;
		private Point offset;

		#region DependencyProperty changed handlers

		private static void GenericRenderInvalidatingPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
		{
			RadialLayoutPanel panel = (RadialLayoutPanel)obj;
			panel.Update();
		}

		private static void ItemsSkippedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			if (!DependencyHelper.CoerceMinimum(d, e, ItemsSkippedProperty, 0))
			{
				((RadialLayoutPanel)d).Update();
			}
		}

		private static void ItemsMaximumPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			if (!DependencyHelper.CoerceMinimum(d, e, ItemsMaximumProperty, 1))
			{
				((RadialLayoutPanel)d).Update();
			}
		}

		#endregion

		#region Properties for dependency properties

        /// <summary>
        /// Gets or sets the length of the radius at the start angle, in pixels.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double RadiusBegin
		{
			get { return (double)this.GetValue(RadiusBeginProperty); }
			set { this.SetValue(RadiusBeginProperty, value); }
		}

        /// <summary>
        /// Gets or sets the length of the radius at the end angle, in pixels.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double RadiusEnd
		{
			get { return (double)this.GetValue(RadiusEndProperty); }
			set { this.SetValue(RadiusEndProperty, value); }
		}

        /// <summary>
        /// Gets or sets the starting angle in degrees according to Cartesian coordinates.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double AngleBegin
		{
			get { return (double)this.GetValue(AngleBeginProperty); }
			set { this.SetValue(AngleBeginProperty, value); }
		}

        /// <summary>
        /// Gets or sets the ending angle in degrees according to Cartesian coordinates.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double AngleEnd
		{
			get { return (double)this.GetValue(AngleEndProperty); }
			set { this.SetValue(AngleEndProperty, value); }
		}

        /// <summary>
        /// Gets or sets the rotation in degrees of the first child element.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double ChildAngleBegin
		{
			get { return (double)this.GetValue(ChildAngleBeginProperty); }
			set { this.SetValue(ChildAngleBeginProperty, value); }
		}

        /// <summary>
        /// Gets or sets the easing that determines how intermediate values of radius and angle are calculated between start and end.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public EasingFunctionBase ChildAngleEasing
        {
            get { return (EasingFunctionBase)this.GetValue(ChildAngleEasingProperty); }
            set { this.SetValue(ChildAngleEasingProperty, value); }
        }

        /// <summary>
        /// Gets or sets the rotation in degrees of a child element relative to the previous one.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double ChildAngleDelta
		{
			get { return (double)this.GetValue(ChildAngleDeltaProperty); }
			set { this.SetValue(ChildAngleDeltaProperty, value); }
		}

        /// <summary>
        /// Gets or sets the easing that determines how intermediate values of radius and angle are calculated between start and end.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
		public EasingFunctionBase Easing
		{
			get { return (EasingFunctionBase)this.GetValue(EasingProperty); }
			set { this.SetValue(EasingProperty, value); }
		}

        /// <summary>
        /// Gets or sets the number of initial child elements to omit from the layout.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
		public int ItemsSkipped
		{
			get { return (int)this.GetValue(ItemsSkippedProperty); }
			set { this.SetValue(ItemsSkippedProperty, value); }
		}

        /// <summary>
        /// Gets or sets the maximum number of child elements to include in the layout.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
		public int ItemsMaximum
		{
			get { return (int)this.GetValue(ItemsMaximumProperty); }
			set { this.SetValue(ItemsMaximumProperty, value); }
		}


        /// <summary>
        /// Gets the X origin relative to the left of the shape area.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public double OriginX
        {
            get { return (double)this.GetValue(OriginXProperty); }
            private set { this.SetValue(OriginXProperty, value); }
        }

        /// <summary>
        /// Gets the Y origin relative to the top of the shape area.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public double OriginY
        {
            get { return (double)this.GetValue(OriginYProperty); }
            private set { this.SetValue(OriginYProperty, value); }
        }

        /// <summary>
        /// Determines whether the origin should be centered within the width and height of the control.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
        public bool CenterOrigin
        {
            get { return (bool)this.GetValue(CenterOriginProperty); }
            set { this.SetValue(CenterOriginProperty, value); }
        }

        /// <summary>
        /// Sets the NormalizedAngle attached property on a UIElement that is a child of the RadialLayoutPanel
        /// </summary>
        /// <param name="element">The UIElement that the attached property is set</param>
        /// <param name="value">The value of the normalized angle</param>
		public static void SetNormalizedAngle(UIElement element, double value)
		{
			element.SetValue(NormalizedAngleProperty, value);
		}

        /// <summary>
        /// Gets the NormalizedAngle attached property on a UIElement that is a child of the RadialLayoutPanel
        /// </summary>
        /// <param name="element">The UIElement that the attached property is set</param>
        /// <returns>The value of the normalized angle</returns>
		public static double GetNormalizedAngle(UIElement element)
		{
			return (double)element.GetValue(NormalizedAngleProperty);
		}

		#endregion

		private void Update()
		{
			// Generic handler
			this.InvalidateMeasure();
		}

		/// <summary>
		/// Tries to retrieves the normalized angle of the element through an attached property.
		/// If not specified, then the normalized angle is directly propertional to the index of the element.
		/// </summary>
        /// <param name="child"></param>
		/// <returns></returns>
		private double GetNormalizedAngleOfChild(UIElement child)
		{
			double normalizedAngle = (double)GetNormalizedAngle(child);

			if (double.IsNaN(normalizedAngle) || double.IsInfinity(normalizedAngle))
			{
				normalizedAngle = MathHelper.NormalValue(0, this.Children.Count - 1, this.Children.IndexOf(child));
			}

			return normalizedAngle;
		}

		protected override Size MeasureOverride(Size availableSize)
		{
			Size childSize = new Size(Double.PositiveInfinity, Double.PositiveInfinity);

			double? minX = null, minY = null, maxX = null, maxY = null;

			int includedChildren = 0;
						
			for (int childIndex = 0; childIndex < this.Children.Count; childIndex++)
			{
				UIElement child = this.Children[childIndex];

				if (childIndex < this.ItemsSkipped || includedChildren >= this.ItemsMaximum)
				{
					child.Measure(Size.Empty);
					continue;	// don't break because we still want to measure
				}

				includedChildren++;

				child.Measure(childSize);

				double normalizedAngle = this.GetNormalizedAngleOfChild(child);
				Point point = RadialHelper.GetRadialCoordinateForDegrees(this.AngleBegin, this.AngleEnd, this.RadiusBegin, this.RadiusEnd, normalizedAngle, this.Easing);

                double childAngle = this.GetChildAngle(childIndex);

                Size size = LayoutHelper.GetSizeAtAngle(child.DesiredSize, childAngle);
                                                
                Point leftTop = LayoutHelper.GetLeftTopAtAngle(child.DesiredSize, childAngle, child.RenderTransformOrigin);

                minX = MathHelper.Minimum(minX, point.X + leftTop.X);
                minY = MathHelper.Minimum(minY, point.Y + leftTop.Y);
                maxX = MathHelper.Maximum(maxX, point.X + (size.Width + leftTop.X));
                maxY = MathHelper.Maximum(maxY, point.Y + (size.Height + leftTop.Y));
			}

			this.computedWidth = minX.HasValue && maxX.HasValue ? maxX.Value - minX.Value : 0.0;
			this.computedHeight = minY.HasValue && maxY.HasValue ? maxY.Value - minY.Value : 0.0;

            // Align at top left
			this.offset = new Point(minX.HasValue ? -minX.Value : 0.0, minY.HasValue ? -minY.Value : 0.0);

            double totalWidth = this.Width;

            if (Double.IsNaN(totalWidth))
            {
                if (this.HorizontalAlignment == HorizontalAlignment.Stretch && !double.IsInfinity(availableSize.Width))
                {
                    totalWidth = availableSize.Width;
                }
                else
                {
                    totalWidth = this.computedWidth;
                }
            }

            double totalHeight = this.Height;
            
            if (Double.IsNaN(totalHeight))
            {
                if (this.VerticalAlignment == VerticalAlignment.Stretch && !double.IsInfinity(availableSize.Height))
                {
                    totalHeight = availableSize.Height;
                }
                else
                {
                    totalHeight = this.computedHeight;
                }
            }
            
			return new Size(totalWidth, totalHeight);
		}


		protected override Size ArrangeOverride(Size finalSize)
		{
            int childrenCount = this.Children.Count;

			int includedChildren = 0;

            double totalWidth = Double.IsNaN(this.Width) ? (this.HorizontalAlignment == HorizontalAlignment.Stretch ? finalSize.Width : this.computedWidth) : this.Width;
			double totalHeight = Double.IsNaN(this.Height) ? (this.VerticalAlignment == VerticalAlignment.Stretch ? finalSize.Height : this.computedHeight) : this.Height;

            if (this.CenterOrigin)
            {
                this.OriginX = totalWidth.Halved();
                this.OriginY = totalHeight.Halved();
            }
            else
            {
                this.OriginX = this.offset.X;
                this.OriginY = this.offset.Y;
            }

            Point origin = new Point(this.OriginX, this.OriginY);

            for (int childIndex = 0; childIndex < childrenCount; childIndex++)
			{				
				UIElement child = this.Children[childIndex];

				if (childIndex < this.ItemsSkipped || includedChildren >= this.ItemsMaximum)
				{
                    child.Arrange(EmptyRect);
					continue;
				}

				includedChildren++;
								
				double normalizedAngle = this.GetNormalizedAngleOfChild(child);
                Point point = RadialHelper.GetRadialCoordinateForDegrees(this.AngleBegin, this.AngleEnd, this.RadiusBegin, this.RadiusEnd, normalizedAngle, this.Easing);

                double childAngle = this.GetChildAngle(childIndex);
                LayoutHelper.SetRotation(child, childAngle);

                // Translate to align RenderTransformOrigin with point. The former is defined for angle 0 and we must translate accordingly.
                Point leftTop = LayoutHelper.GetLeftTopAtAngle(child.DesiredSize, 0.0, child.RenderTransformOrigin);
                LayoutHelper.SetTranslate(child, leftTop.X, leftTop.Y);

                child.Arrange(new Rect(LayoutHelper.GetOffsetPoint(point, origin), child.DesiredSize));
			}

			
			return new Size(totalWidth, totalHeight);
		}

        private double GetChildAngle(int childIndex)
        {
            int childrenCount = this.Children.Count;
            double childAngle;

            if (this.ChildAngleEasing == null)
            {
                childAngle = this.ChildAngleBegin + childIndex * this.ChildAngleDelta;
            }
            else
            {
                double ease = childrenCount == 1 ? 0.0 : this.ChildAngleEasing.Ease((double)childIndex / (double)(childrenCount - 1));
                childAngle = this.ChildAngleBegin + ease * this.ChildAngleDelta * (childrenCount - 1);
            }
            return childAngle;
        }
	}
}

